/*
 * Copyright 2010 Gaurav Saxena
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.gwtstructs.gwt.client.widgets.singleselectlistbox;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.HasAllFocusHandlers;
import com.google.gwt.event.dom.client.HasAllKeyHandlers;
import com.google.gwt.event.dom.client.HasAllMouseHandlers;
import com.google.gwt.event.dom.client.HasChangeHandlers;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.MouseWheelHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FormPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.gwtstructs.gwt.client.widgets.singleselectlistbox.ListBoxItem;

public class HtmlEnabledListBox<T> extends Composite implements HasChangeHandlers, HasAllFocusHandlers
, HasAllKeyHandlers, HasAllMouseHandlers{
	private List<ListBoxItem<T>> listBoxItems;
	private int selectedIndex = -1;
	private VerticalPanel suggestionBox;
	private Anchor listBox;
	private String pullDownImageUrl = "";
	private String hoverPullDownImageUrl = "";

	public HtmlEnabledListBox()
	{
		this(new ArrayList<ListBoxItem<T>>());
	}
	public HtmlEnabledListBox(List<ListBoxItem<T>> listBoxItems)
	{
		VerticalPanel vp = new VerticalPanel();
		this.listBoxItems = listBoxItems;
		listBox = new Anchor("&nbsp;");
		vp.add(listBox);
		listBox.setStyleName("gwtstrutcs-singleselectlistbox");
		DOM.setStyleAttribute(listBox.getElement(), "background", "white url('"+ pullDownImageUrl +"') no-repeat 100% center");
		DOM.setStyleAttribute(listBox.getElement(), "border", "solid 1px #7f9db9");
		DOM.setStyleAttribute(listBox.getElement(), "color", "black");
		DOM.setStyleAttribute(listBox.getElement(), "textDecoration", "none");
		DOM.setStyleAttribute(listBox.getElement(), "cursor", "default");
		DOM.setStyleAttribute(listBox.getElement(), "display", "block");
		listBox.addMouseOverHandler(new MouseOverHandler(){
			@Override
			public void onMouseOver(MouseOverEvent event) {
				DOM.setStyleAttribute(listBox.getElement(), "backgroundImage", "url('" + hoverPullDownImageUrl + "')");
		}});
		listBox.addMouseOutHandler(new MouseOutHandler(){
			@Override
			public void onMouseOut(MouseOutEvent event) {
				DOM.setStyleAttribute(listBox.getElement(), "backgroundImage", "url('" + pullDownImageUrl + "')");
		}});
		listBox.addClickHandler(new ClickHandler(){
			@Override
			public void onClick(ClickEvent event) {
				if(suggestionBox.isVisible())
					suggestionBox.setVisible(false);
				else
				{
					suggestionBox.setVisible(true);
					suggestionBox.setPixelSize(getOffsetWidth(), Math.min(200, suggestionBox.getOffsetHeight()));
					if(selectedIndex > -1)
						makeSelected(true, (HTML) suggestionBox.getWidget(selectedIndex));
				}
			}
		});
		listBox.addBlurHandler(new BlurHandler(){
			@Override
			public void onBlur(BlurEvent event) {
					Timer t = new Timer(){
					@Override
					public void run() {
						if(isValidBlur())
							suggestionBox.setVisible(false);
					}};
					t.schedule(100);
		}});
		suggestionBox = new VerticalPanel();
		createSuggestionBox();
		vp.add(suggestionBox);
		setUpSuggestionPanelEvents(suggestionBox.getElement(), listBox.getElement());
		DOM.setStyleAttribute(listBox.getElement(), "paddingRight", "20px");
		initWidget(vp);
	}
	@Override
	public void onLoad()
	{
		super.onLoad();
		DOM.setStyleAttribute(listBox.getElement(), "width", getOffsetWidth() - 20 + "px");
		DOM.setStyleAttribute(suggestionBox.getElement(), "width", listBox.getOffsetWidth() + "px");
	}
	private native boolean isValidBlur() /*-{
		return window.isValidBlur;
	}-*/;
	private native void setUpSuggestionPanelEvents(Element suggestionPanel, Element textBox) /*-{
		window.isValidBlur = true;
		suggestionPanel.onmousedown = function(){
			window.isValidBlur = false;
		};
		suggestionPanel.onmouseout = function(){
			textBox.focus();
			window.isValidBlur = true;
		};
	}-*/;
	private void createSuggestionBox() {
		for (int i = 0 ;i < listBoxItems.size(); i++)
		{
			final HTML option = new HTML(listBoxItems.get(i).getDisplayNameHtml());
			suggestionBox.add(option);
			DOM.setElementAttribute(option.getElement(), "value", listBoxItems.get(i).getValue().toString());
			option.addMouseOverHandler(new MouseOverHandler(){
				@Override
				public void onMouseOver(MouseOverEvent event) {
					if(selectedIndex > -1)
						makeSelected(false, (HTML) suggestionBox.getWidget(selectedIndex));
					makeSelected(true, option);
			}});
			option.addMouseOutHandler(new MouseOutHandler(){
				@Override
				public void onMouseOut(MouseOutEvent event) {
					makeSelected(false, option);
			}});
			final int j = i;
			option.addClickHandler(new ClickHandler(){
				@Override
				public void onClick(ClickEvent event) {
					makeSelected(true, option);
					if(j != getSelectedIndex())
					{
						setSelectedIndex(j);
						fireEvent(new ChangeEvent(){});
					}
					suggestionBox.setVisible(false);
			}});
			suggestionBox.setVisible(false);
		}
		DOM.setStyleAttribute(suggestionBox.getElement(), "position", "absolute");
		suggestionBox.setStyleName("gwtstrutcs-singleselectlistbox-suggestionBox");
	}
	private void makeSelected(boolean isSelected, HTML option) {
		if(isSelected)
		{
			option.getElement().removeClassName("gwtstrutcs-singleselectlistbox-unselected");
			option.getElement().addClassName("gwtstrutcs-singleselectlistbox-selected");
		}
		else
		{
			option.getElement().removeClassName("gwtstrutcs-singleselectlistbox-selected");
			option.getElement().addClassName("gwtstrutcs-singleselectlistbox-unselected");
		}
	}
	private void resetListBox() {
		suggestionBox.clear();
		createSuggestionBox();
	}
	@Override
	public HandlerRegistration addChangeHandler(ChangeHandler handler) {
		return addDomHandler(handler, ChangeEvent.getType());
	}
	@Override
	public HandlerRegistration addFocusHandler(FocusHandler handler) {
		return listBox.addFocusHandler(handler);
	}
	@Override
	public HandlerRegistration addBlurHandler(BlurHandler handler) {
		return listBox.addBlurHandler(handler);
	}
	@Override
	public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
		return listBox.addKeyUpHandler(handler);
	}
	@Override
	public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
		return listBox.addKeyDownHandler(handler);
	}
	@Override
	public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
		return listBox.addKeyPressHandler(handler);
	}
	@Override
	public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
		return listBox.addMouseDownHandler(handler);
	}
	@Override
	public HandlerRegistration addMouseUpHandler(MouseUpHandler handler) {
		return listBox.addMouseUpHandler(handler);
	}
	@Override
	public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) {
		return listBox.addMouseOutHandler(handler);
	}
	@Override
	public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) {
		return listBox.addMouseOverHandler(handler);
	}
	@Override
	public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
		return listBox.addMouseMoveHandler(handler);
	}
	@Override
	public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) {
		return listBox.addMouseWheelHandler(handler);
	}
	/**
	 * @return the current listboxitems
	 */
	public List<ListBoxItem<T>> getListBoxItems() {
		return listBoxItems;
	}
	/**
	 * sets the index of selected item
	 * @param index
	 */
	public void setSelectedIndex(int index)
	{
		for (int i = 0 ;i < listBoxItems.size(); i++)
			listBoxItems.get(i).setSelected(false);
		listBoxItems.get(index).setSelected(true);
		listBox.setHTML(listBoxItems.get(index).getDisplayNameHtml());
		selectedIndex = index;
	}
	/**
	 * @return index of selected item
	 */
	public int getSelectedIndex()
	{
		return selectedIndex;
	}
	/**
	 * Sets the text which is shown when none of the options are selected. This text doesn't show up in the
	 * options
	 * @param defaultText
	 */
	public void setDefaultText(String defaultText)
	{
		listBox.setHTML(defaultText);
	}
	/**
	 * @param index
	 * @return the option {@link HTML}
	 */
	public HTML getOptionWidget(int index)
	{
		return (HTML) suggestionBox.getWidget(index);
	}
	/**
	 * Returns the current pull down image url. Default value of this is base 64 encoded image resembling pull 
	 * down image in WIndows XP.
	 * @return
	 */
	public String getPullDownImageUrl() {
		return pullDownImageUrl;
	}
	/**
	 * sets the current pull down image url 
	 * @param pullDownImageUrl
	 */
	public void setPullDownImageUrl(String pullDownImageUrl) {
		this.pullDownImageUrl = pullDownImageUrl;
	}
	/**
	 * Returns the current pull down image url on hover . Default value of this is base 64 encoded image 
	 * resembling pull down image on hover in WIndows XP.
	 * @return
	 */
	public String getHoverPullDownImageUrl() {
		return hoverPullDownImageUrl;
	}
	/**
	 * sets the current pull down image url on hover
	 * @param hoverPullDownImageUrl
	 */
	public void setHoverPullDownImageUrl(String hoverPullDownImageUrl) {
		this.hoverPullDownImageUrl = hoverPullDownImageUrl;
	}
	/**
	  * Adds an item to the list box.
	  * @param item the {@link ListBoxItem} to be added
	  */
	public void addItem(ListBoxItem<T> item) {
		addItem(item, listBoxItems.size());
	}
	/**
	  * Adds an item to the list box, specifying an initial value for the item.
	  * 
	  * @param item the text of the item to be added
	  * @param value the item's value, to be submitted if it is part of a
	  *          {@link FormPanel}; cannot be <code>null</code>
	  */
	public void addItem(ListBoxItem<T> item, int position) {
	    listBoxItems.add(position, item);
	    if(position <= selectedIndex)
	    	setSelectedIndex(++selectedIndex);
	    resetListBox();
	}
	/**
	  * Removes all items from the list box.
	  */
	 public void clear() {
		 listBoxItems.clear();
		 resetListBox();
	 }
	 /**
	  * Gets the number of items present in the list box.
	  * 
	  * @return the number of items
	  */
	 public int getItemCount() {
		 return listBoxItems.size();
	 }
	 /**
	   * Gets the text associated with the item at the specified index.
	   * 
	   * @param index the index of the item whose text is to be retrieved
	   * @return the text associated with the item
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public String getItemText(int index) {
		  checkIndex(index);
		  return DOM.getInnerText(getOptionWidget(index).getElement());
	  }
	  /**
	   * Gets the text associated with the item at the specified index.
	   * 
	   * @param index the index of the item whose text is to be retrieved
	   * @return the text associated with the item
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public String getItemHtml(int index) {
	    checkIndex(index);
	    return listBoxItems.get(index).getDisplayNameHtml();
	  }
	  /**
	   * Gets the value associated with the item at a given index.
	   * 
	   * @param index the index of the item to be retrieved
	   * @return the item's associated value
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public T getValue(int index) {
		  checkIndex(index);
		    return listBoxItems.get(index).getValue();
	  }
	  private void checkIndex(int index) {
	    if (index < 0 || index >= listBoxItems.size()) {
	      throw new IndexOutOfBoundsException();
	    }
	  }
	  /**
	   * Determines whether an individual list item is selected.
	   * 
	   * @param index the index of the item to be tested
	   * @return <code>true</code> if the item is selected
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public boolean isItemSelected(int index) {
	    checkIndex(index);
	    return listBoxItems.get(index).isSelected();
	  }
	  /**
	   * Removes the item at the specified index.
	   * 
	   * @param index the index of the item to be removed
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public void removeItem(int index) {
		  checkIndex(index);
		  listBoxItems.remove(index);
		  resetListBox();
	  }
	  /**
	   * Sets whether an individual list item is selected.
	   * 
	   * <p>
	   * Note that setting the selection programmatically does <em>not</em> cause
	   * the {@link ChangeHandler#onChange(ChangeEvent)} event to be fired.
	   * </p>
	   * 
	   * @param index the index of the item to be selected or unselected
	   * @param selected <code>true</code> to select the item
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public void setItemSelected(int index, boolean selected) {
	    checkIndex(index);
	    listBoxItems.get(index).setSelected(selected);
	    deselectAll();
	    makeSelected(selected, getOptionWidget(index));
	  }
	  private void deselectAll() {
		  int optionLength = suggestionBox.getWidgetCount();
		  for(int j = 0; j < optionLength; j++)
				  makeSelected(false, (HTML) getOptionWidget(j));
		  for(int i = 0; i < listBoxItems.size(); i++)
			  listBoxItems.get(i).setSelected(false);
	  }
	 /**
	   * Sets the an item with the html at a given index. Value of the listItem stays the same
	   * 
	   * @param index the index of the item to be set
	   * @param text the item's new text
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public void setItemHtml(int index, String html) {
	    checkIndex(index);
	    if (html == null) {
	      throw new NullPointerException("Cannot set an option to have null text");
	    }
	    listBoxItems.get(index).setDisplayNameHtml(html);
	    DOM.setInnerHTML(getOptionWidget(index).getElement(), html);
	  }
	  /**
	   * Sets the value associated with the item at a given index. 
	   * 
	   * @param index the index of the item to be set
	   * @param value the item's new value; cannot be <code>null</code>
	   * @throws IndexOutOfBoundsException if the index is out of range
	   */
	  public void setValue(int index, T value) {
		  //This value can be
		  //used for any purpose, but is also what is passed to the server when the
		  //list box is submitted as part of a {@link FormPanel}.
		  checkIndex(index);
		  DOM.setElementAttribute(getOptionWidget(index).getElement(), "value", value.toString());
		  listBoxItems.get(index).setValue(value);
	  }
	
	/**
	 * Sets up new set of listbox items which the drop down shows instantaneously
	 * @param listBoxItems
	 */
	  public void setListBoxItems(List<ListBoxItem<T>> listBoxItems) {
		  this.listBoxItems = listBoxItems;
		  resetListBox();
	  }
	  /**
	   * @param focused
	   */
	  public void setFocus(boolean focused)
	  {
		  listBox.setFocus(focused);
	  }
	/**
	 * @param the tabIndex
	 */
	  public void setTabIndex(int index) {
		  listBox.setTabIndex(index);
	  }
	/**
	 * @return the tabIndex
	 */
	  public int getTabIndex()
	  {
		  return listBox.getTabIndex();
	  }
	/**
	 * @param key
	 */
	  public void setAccessKey(char key)
	  {
		  listBox.setAccessKey(key);
	  }
}
